﻿using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.IO;
using SimpleX.Middlewares;
using System.IO;
using UAParser;

//说明:RequestMiddleware 全局请求/返回 相关代码参考WatchDog
//感谢https://github.com/IzyPro/WatchDog

namespace SimpleX
{
    /// <summary>
    /// RequestMiddleware一个中间件 包含了 全局错误管理,全局请求/返回 记录
    /// </summary>
    public class RequestMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
        private readonly ILogger<RequestMiddleware> _logger;

        public RequestMiddleware(RequestDelegate next, ILogger<RequestMiddleware> logger)
        {
            _next = next;
            _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
            _logger = logger;
        }

        public async Task Invoke(HttpContext context)
        {
            //是否启用http日志
            var enableLog = App.Get<bool>("SystemConfig:RequestMiddlewareHttpLog");
            var ignoreLog = context.GetMetadata<IgnoreLogAttribute>() != null;
            enableLog = enableLog && !ignoreLog;

            //处理请求和相应
            var requestLog = await LogRequest(context, enableLog);
            var responseLog = await LogResponse(context, requestLog, enableLog);

            //不启用不记录http日志
            if (!enableLog)
                return;

            var timeSpent = responseLog.FinishTime.Subtract(requestLog.StartTime);
            var clientInfo = requestLog.ClientInfo;

            var fullModel = new FullModel
            {
                OpIp = requestLog.IpAddress,
                Name = requestLog.RequestName,
                ReqMethod = requestLog.CodeMethodName,
                ReqHeaders = requestLog.RequestHeaders,
                ReqUrl = requestLog.Path,
                ParamJson = requestLog.RequestBody,
                ResultJson = responseLog.ResponseBody,
                ResultHeaders = responseLog.ResponseHeaders,
                TimeSpentStr = string.Format("{0:D1} hrs {1:D1} mins {2:D1} secs {3:D1} ms", timeSpent.Hours, timeSpent.Minutes, timeSpent.Seconds, timeSpent.Milliseconds),
                TimeSpent = timeSpent.TotalMilliseconds,
                ResponseStatus = responseLog.ResponseStatus,
                OpTime = requestLog.StartTime,
                OpFinishTime = responseLog.FinishTime,
                OpAccount = requestLog.Account,
                OpUser = requestLog.UserName,
                OpUserId = requestLog.UserId,
                MethodName = requestLog.CodeMethodName,
                ClassName = requestLog.ClassName,
                Category = "登录/登出/操作/异常",
                ExeMessage = "异常信息",
                Exception = responseLog.Exception,
                OpAddress = IPHelper.GetAddress(requestLog.IpAddress),
                OpBrowser = clientInfo?.UA?.Family + clientInfo?.UA?.Major,
                OpOs = clientInfo?.OS?.Family + clientInfo?.OS?.Major,
            };

            await WriteToDb(fullModel);
        }

        private async Task WriteToDb(FullModel fullModel)
        {
            try
            {
                #region Process Field

                if (fullModel.Exception != null)
                    fullModel.ExeMessage = $"Message:{fullModel.Exception?.Message}\nStackTrace:{fullModel.Exception?.StackTrace}";
                else
                    fullModel.ExeMessage = null;

                if (fullModel.Exception == null)
                {
                    if (fullModel.Name == "LoginIn")
                        fullModel.Category = "LOGIN";
                    else if (fullModel.Name == "LoginOut")
                        fullModel.Category = "LOGOUT";
                    else
                        fullModel.Category = "OPERATE";
                }
                else
                    fullModel.Category = "EXCEPTION";

                #endregion

                var connection = DbContext.Db.GetConnection(SqlsugarConst.DB_Default);
                await connection.Insertable(fullModel).ExecuteCommandAsync();
            }
            catch
            {
            }
        }

        private async Task<RequestModel> LogRequest(HttpContext context, bool enableLog)
        {
            var startTime = DateTime.Now;

            var requestBodyDto = new RequestModel()
            {
                RequestBody = string.Empty,
                //Host = context.Request.Host.ToString(),
                IpAddress = context.Connection.RemoteIpAddress?.ToString(),
                Path = context.Request.Path.ToString(),
                CodeMethodName = context.Request.Method.ToString(),
                QueryString = context.Request.QueryString.ToString(),
                StartTime = startTime,
                RequestHeaders = context.Request.Headers.Select(x => x.ToString()).Aggregate((a, b) => a + ": " + b),
                UserId = context?.User.FindFirst(ClaimConst.UserId)?.Value,
                UserName = context?.User.FindFirst(ClaimConst.Name)?.Value,
                Account = context?.User.FindFirst(ClaimConst.Account)?.Value,
                RequestName = context.GetMetadata<ActionPermissionAttribute>()?.Name,
            };

            if (!string.IsNullOrWhiteSpace(requestBodyDto.IpAddress))
                requestBodyDto.IpAddress = requestBodyDto.IpAddress.Replace("::ffff:", "");

            if (requestBodyDto.IpAddress == "::1")
                requestBodyDto.IpAddress = "127.0.0.1";

            if (context.Request.Headers.TryGetValue("User-Agent", out var userAgent))
            {
                var clientInfo = Parser.GetDefault().Parse(userAgent);
                requestBodyDto.ClientInfo = clientInfo;
            }

            var endpoint = context.GetEndpoint();
            var actionDescriptor = endpoint?.Metadata.GetMetadata<ControllerActionDescriptor>();

            if (actionDescriptor != null)
            {
                var methodInfo = actionDescriptor.MethodInfo;
                requestBodyDto.ClassName = methodInfo.DeclaringType?.FullName;
                requestBodyDto.CodeMethodName = methodInfo.Name;
            }

            if (context.Request.ContentLength > 1 && enableLog)
            {
                context.Request.EnableBuffering();
                await using var requestStream = _recyclableMemoryStreamManager.GetStream();
                await context.Request.Body.CopyToAsync(requestStream);
                requestBodyDto.RequestBody = StreamHelper.ReadStreamInChunks(requestStream);
                context.Request.Body.Position = 0;
            }
            return requestBodyDto;
        }

        private async Task<ResponseModel> LogResponse(HttpContext context, RequestModel requestModel, bool enableLog)
        {
            if (!enableLog)
            {
                await Next(context, requestModel);
                return default;
            }

            using (var originalBodyStream = context.Response.Body)
            {
                try
                {
                    using (var originalResponseBody = _recyclableMemoryStreamManager.GetStream())
                    {
                        var responseBodyDto = new ResponseModel();

                        context.Response.Body = originalResponseBody;

                        await Next(context, requestModel, ex =>
                        {
                            responseBodyDto.Exception = ex;
                        });

                        context.Response.Body.Seek(0, SeekOrigin.Begin);
                        var responseBody = await new StreamReader(context.Response.Body).ReadToEndAsync();
                        context.Response.Body.Seek(0, SeekOrigin.Begin);

                        responseBodyDto.ResponseStatus = context.Response.StatusCode;
                        responseBodyDto.FinishTime = DateTime.Now;
                        responseBodyDto.ResponseHeaders = context.Response.Headers.ContentLength > 0 ? context.Response.Headers.Select(x => x.ToString()).Aggregate((a, b) => a + ": " + b) : string.Empty;

                        //如果是文件只记录文件名
                        if (context.Response.Headers.ContainsKey("Content-Disposition"))
                            responseBodyDto.ResponseBody = context.Response.Headers.ContentDisposition.ToString();
                        else
                            responseBodyDto.ResponseBody = responseBody;

                        await originalResponseBody.CopyToAsync(originalBodyStream);
                        return responseBodyDto;
                    }
                }
                catch (Exception ex)
                {
                    return new ResponseModel
                    {
                        ResponseBody = $"Read Response Body Error:{ex.Message}",
                        ResponseStatus = context.Response.StatusCode,
                        FinishTime = DateTime.Now,
                        ResponseHeaders = context.Response.Headers.ContentLength > 0 ? context.Response.Headers.Select(x => x.ToString()).Aggregate((a, b) => a + ": " + b) : string.Empty,
                    };
                }
                finally
                {
                    context.Response.Body = originalBodyStream;
                }
            }
        }

        private async Task Next(HttpContext context, RequestModel requestModel, Action<Exception> onError = null)
        {
            try
            {
                await _next(context);
            }
            catch (Exception ex)
            {
                onError?.Invoke(ex);

                //业务异常统一400且不记录日志
                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                context.Response.ContentType = "application/json";

                var error = "InternalServerError";

                if (App.Configuration.GetValue<bool>("SystemConfig:ShowInternalServerError"))
                {
                    error = ex.Message;
                }

                //记录接口耗时
                //_ = long.TryParse(context.Items["__StopwatchMiddlewareWithEndpointExceptioned"]?.ToString(), out long elapsedMilliseconds);

                var result = new UnifyResult<string>()
                {
                    Code = HttpStatusCode.InternalServerError,
                    Exts = Unify.Read(),
                    Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                    Msg = error,
                    ElapsedMilliseconds = (long)(DateTime.Now - requestModel.StartTime).TotalMilliseconds
                };

                //业务异常统一400且不记录日志
                if (ex.GetType() == typeof(BusinessException))
                {
                    context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                    result.Code = HttpStatusCode.BadRequest;
                }
                else
                {
                    _logger.LogError(exception: ex, message: $"Request To Json: {requestModel.ToJson()}");
                }

                await context.Response.WriteAsync(result.ToJson());
            }
        }
    }
}